/** * (C) Copyright 2013 Jabylon (http://www.jabylon.org) and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package org.jabylon.rest.ui.wicket.panels; import java.io.File; import java.io.IOException; import java.text.DateFormat; import java.text.MessageFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; import javax.inject.Inject; import javax.servlet.http.HttpServletResponse; import org.apache.wicket.AttributeModifier; import org.apache.wicket.MarkupContainer; import org.apache.wicket.Session; import org.apache.wicket.behavior.AttributeAppender; import org.apache.wicket.extensions.markup.html.repeater.data.table.filter.IFilterStateLocator; import org.apache.wicket.extensions.markup.html.repeater.util.SortableDataProvider; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.form.StatelessForm; import org.apache.wicket.markup.html.form.upload.FileUpload; import org.apache.wicket.markup.html.form.upload.FileUploadField; import org.apache.wicket.markup.html.link.BookmarkablePageLink; import org.apache.wicket.markup.html.link.ExternalLink; import org.apache.wicket.markup.html.link.StatelessLink; import org.apache.wicket.markup.html.list.ListItem; import org.apache.wicket.markup.html.list.ListView; import org.apache.wicket.markup.repeater.RepeatingView; import org.apache.wicket.model.CompoundPropertyModel; import org.apache.wicket.model.IModel; import org.apache.wicket.model.LoadableDetachableModel; import org.apache.wicket.model.Model; import org.apache.wicket.model.StringResourceModel; import org.apache.wicket.request.http.flow.AbortWithHttpErrorCodeException; import org.apache.wicket.request.mapper.parameter.PageParameters; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.emf.cdo.util.CommitException; import org.eclipse.emf.common.util.EList; import org.eclipse.emf.common.util.URI; import org.jabylon.cdo.connector.Modification; import org.jabylon.cdo.connector.TransactionUtil; import org.jabylon.common.resolver.URIResolver; import org.jabylon.common.review.ReviewParticipant; import org.jabylon.common.util.URLUtil; import org.jabylon.properties.Comment; import org.jabylon.properties.Project; import org.jabylon.properties.ProjectLocale; import org.jabylon.properties.ProjectVersion; import org.jabylon.properties.PropertiesFactory; import org.jabylon.properties.Property; import org.jabylon.properties.PropertyFile; import org.jabylon.properties.PropertyFileDescriptor; import org.jabylon.properties.Review; import org.jabylon.properties.ReviewState; import org.jabylon.properties.Severity; import org.jabylon.properties.util.PropertyResourceUtil; import org.jabylon.resources.persistence.PropertyPersistenceService; import org.jabylon.rest.ui.Activator; import org.jabylon.rest.ui.model.EClassSortState; import org.jabylon.rest.ui.model.EObjectModel; import org.jabylon.rest.ui.model.PropertyPair; import org.jabylon.rest.ui.security.CDOAuthenticatedSession; import org.jabylon.rest.ui.security.RestrictedComponent; import org.jabylon.rest.ui.util.WicketUtil; import org.jabylon.rest.ui.wicket.BasicResolvablePanel; import org.jabylon.rest.ui.wicket.components.ConfirmBehaviour; import org.jabylon.rest.ui.wicket.pages.ResourcePage; import org.jabylon.security.CommonPermissions; import org.jabylon.users.User; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Multimap; public class PropertyListPanel extends BasicResolvablePanel<PropertyFileDescriptor> implements RestrictedComponent { private static final long serialVersionUID = 1L; IModel<Multimap<String, Review>> reviewModel; static final String OK_LABEL = "OK"; private static final Logger logger = LoggerFactory.getLogger(PropertyPairListDataProvider.class); @Inject List<ReviewParticipant> reviewParticipants; @Inject private PropertyPersistenceService propertyPersistence; private PropertyListMode mode; public PropertyListPanel(PropertyFileDescriptor object, PageParameters parameters) { super("content", object, parameters); } @Override protected void construct() { super.construct(); List<ReviewParticipant> activeReviews = PropertyListModeFactory.filterActiveReviews(getModel().getObject().getProjectLocale().getParent().getParent(),reviewParticipants); mode = PropertyListModeFactory.allAsMap(activeReviews).get(getPageParameters().get("mode").toString()); addLinkList(activeReviews,mode); reviewModel = new LoadableDetachableModel<Multimap<String, Review>>() { private static final long serialVersionUID = 1L; @Override protected Multimap<String, Review> load() { return buildReviewMap(getModelObject()); } }; PropertyPairListDataProvider provider = new PropertyPairListDataProvider(getModelObject(), mode, reviewModel); List<PropertyPair> contents = provider.createContents(); ListView<PropertyPair> properties = new ListView<PropertyPair>("repeater", contents) { private static final long serialVersionUID = -7087485011138279358L; @Override protected void populateItem(final ListItem<PropertyPair> item) { IModel<PropertyPair> model = item.getModel(); String key = model.getObject().getKey(); Collection<Review> reviewList = reviewModel.getObject().get(key); BookmarkablePageLink<Object> link = new BookmarkablePageLink<Object>("edit",getPage().getClass(),new PageParameters(getPageParameters()).add("key", key)); item.add(link); link.setMarkupId(URLUtil.escapeToIdAttribute(key)); link.setOutputMarkupId(true); link.add(new AttributeModifier("name", link.getMarkupId())); Label keyLabel = new Label("key", key); keyLabel.add(new AttributeModifier("title", model.getObject().getTranslated())); item.add(keyLabel); Label translationLabel = new Label("translation", model.getObject().getTranslated()); translationLabel.add(new AttributeModifier("title", model.getObject().getTranslatedComment())); item.add(translationLabel); fillStatusColumn(model.getObject(), reviewList, item); } }; properties.setOutputMarkupId(true); add(properties); String contextPath = WicketUtil.getContextPath(); String href = contextPath + "/api/"+ getModelObject().toURI().appendQuery("type=file"); ExternalLink link = new ExternalLink("download.link", href); File file = new File(getModelObject().absoluteFilePath().toFileString()); boolean enabled = file.isFile(); link.setEnabled(enabled); if(!enabled) { link.add(new AttributeAppender("disabled", Model.of("disabled"))); link.add(new AttributeAppender("class", Model.of("disabled"))); } add(link); StatelessLink<Void> deleteLink = new DeleteLink("remove.button"); deleteLink.add(new ConfirmBehaviour(nls("confirm.remove"))); deleteLink.setVisible(canConfigure()); add(deleteLink); FileUploadForm uploadForm = new FileUploadForm("translation-upload-form", getModel()); uploadForm.setVisible(canSuggest()); add(uploadForm); } @Override public String getRequiredPermission() { Project project = getModel().getObject().getProjectLocale().getParent().getParent(); return CommonPermissions.constructPermission(CommonPermissions.PROJECT, project.getName(), CommonPermissions.ACTION_VIEW); } protected void fillStatusColumn(PropertyPair propertyPair, Collection<Review> reviewCollection, MarkupContainer container) { IStatus status = calculateRowStatus(propertyPair); if (status.getSeverity() == IStatus.WARNING) container.add(new AttributeModifier("class", "warning")); else if (status.getSeverity() == IStatus.ERROR) container.add(new AttributeModifier("class", "error")); Collection<Review> reviews = reviewCollection; if (reviews == null || reviews.isEmpty()) reviews = createInMemoryReview(propertyPair); RepeatingView view = new RepeatingView("reviews"); DateFormat formatter = SimpleDateFormat.getDateTimeInstance(SimpleDateFormat.SHORT, SimpleDateFormat.SHORT,getSession().getLocale()); for (Review review : reviews) { if(review.getState()==ReviewState.INVALID || review.getState()==ReviewState.RESOLVED) continue; Label label = new Label(view.newChildId(), review.getReviewType()); label.add(new AttributeAppender("class", getLabelClass(review))); StringBuilder title = new StringBuilder(); if (review.getMessage() != null) title.append(review.getMessage()); if(review.getCreated()>0) { if(title.length()>0) //add a linebreak title.append("\n"); title.append(formatter.format(new Date(review.getCreated()))); } if(title.length()>0) label.add(new AttributeModifier("title", title.toString())); view.add(label); } container.add(view); } private IStatus calculateRowStatus(PropertyPair propertyPair) { if (propertyPair.getOriginal() == null || propertyPair.getOriginal().isEmpty()) return new Status(IStatus.ERROR, "org.jabylon.rest.ui", ""); else if (propertyPair.getTranslated() == null || propertyPair.getTranslated().isEmpty()) return new Status(IStatus.ERROR, "org.jabylon.rest.ui", ""); return Status.OK_STATUS; } private List<Review> createInMemoryReview(PropertyPair pair) { if (pair.getTranslated() == null || pair.getTranslated().isEmpty()) { Review review = PropertiesFactory.eINSTANCE.createReview(); String message = new StringResourceModel("review.missing.translation",this,null,pair.getKey()).getString(); review.setMessage(MessageFormat.format(message, pair.getKey())); review.setReviewType(getString("review.missing.translation.type")); review.setSeverity(Severity.ERROR); return Collections.singletonList(review); } Review review = PropertiesFactory.eINSTANCE.createReview(); review.setReviewType("OK"); return Collections.singletonList(review); } protected String getLabelClass(Review review) { if (OK_LABEL.equals(review.getReviewType())) return " label-success"; Severity severity = review.getSeverity(); switch (severity) { case ERROR: return " label-important"; case INFO: return " label-info"; case WARNING: return " label-warning"; default: return ""; } } /** * checks if the user has permissions to configure this project * @return */ private boolean canConfigure() { return hasPermission(getModel().getObject(), getSession(), CommonPermissions.ACTION_CONFIG); } /** * checks if the user has permissions to make suggestions to this project * @return */ private boolean canSuggest() { return hasPermission(getModel().getObject(), getSession(), CommonPermissions.ACTION_SUGGEST); } private static boolean hasPermission(PropertyFileDescriptor model, Session session, String permission) { ProjectVersion version = model.getProjectLocale().getParent(); if (version.isReadOnly()) return false; if (session instanceof CDOAuthenticatedSession) { Project project = version.getParent(); CDOAuthenticatedSession authSession = (CDOAuthenticatedSession) session; return authSession.hasPermission(CommonPermissions.constructPermission(CommonPermissions.PROJECT, project.getName(), permission)); } return false; } private Multimap<String, Review> buildReviewMap(PropertyFileDescriptor object) { EList<Review> reviews = object.getReviews(); Multimap<String, Review> reviewMap = ArrayListMultimap.create(reviews.size(), 2); for (Review review : reviews) { if(review.getState()==ReviewState.OPEN || review.getState()==ReviewState.REOPENED) reviewMap.put(review.getKey(), review); } return reviewMap; } private void addLinkList(List<ReviewParticipant> activeReviews, final PropertyListMode currentMode) { List<PropertyListMode> values = PropertyListModeFactory.all(activeReviews); ListView<PropertyListMode> mode = new ListView<PropertyListMode>("view-mode", values) { private static final long serialVersionUID = 1L; @Override protected void populateItem(ListItem<PropertyListMode> item) { String mode = item.getModelObject().name(); BookmarkablePageLink<Object> link = new BookmarkablePageLink<Object>("link", getPage().getClass(), new PageParameters(getPageParameters()).set("mode", mode)); ReviewParticipant participant = item.getModel().getObject().getParticipant(); if(participant!=null) link.setBody(nls(participant.getClass(),participant.getName())); else link.setBody(new StringResourceModel(item.getModelObject().name(),item,null)); item.add(link); if (item.getModelObject().equals(currentMode)) item.add(new AttributeModifier("class", "active")); } }; add(mode); } private static class FileUploadForm extends StatelessForm<PropertyFileDescriptor> { private static final long serialVersionUID = -3653084217384164795L; private static final Logger LOG = LoggerFactory.getLogger(FileUploadForm.class); private final ArrayList<FileUpload> uploads; private FileUploadField fileUploadField; public FileUploadForm(String id, IModel<PropertyFileDescriptor> model) { super(id,model); // set this form to multipart mode (always needed for uploads!) setMultiPart(true); uploads = new ArrayList<FileUpload>(); // Add one multi-file upload field @SuppressWarnings({ "unchecked", "rawtypes" }) // IModel<List<FileUpload>> fileModel = new Model<ArrayList<FileUpload>>(uploads); IModel<List<FileUpload>> fileModel = new Model(uploads); fileUploadField = new FileUploadField("upload",fileModel); add(fileUploadField); // Set maximum size to 100K for demo purposes // setMaxSize(Bytes.kilobytes(100)); } @Override protected void onSubmit() { super.onSubmit(); for (FileUpload fileUpload : fileUploadField.getFileUploads()) { PropertyFileDescriptor descriptor = getModel().getObject(); try { PropertyFile file = descriptor.loadProperties(fileUpload.getInputStream()); persist(file); } catch (IOException e) { LOG.error("Failed to retrieve uploaded file for"+descriptor.getLocation(),e); } } } private void persist(PropertyFile file) { PropertyPersistenceService persistenceService = Activator.getDefault().getPersistenceService(); try { PropertyFile original = persistenceService.loadProperties(getModelObject()); PropertyFile master = persistenceService.loadProperties(getModelObject().getMaster()); Map<String, Property> originalProperties = original.asMap(); for(Property prop : master.getProperties()) { if(!originalProperties.containsKey(prop.getKey())) { Property newProperty = PropertiesFactory.eINSTANCE.createProperty(); newProperty.setKey(prop.getKey()); original.getProperties().add(newProperty); } } originalProperties = original.asMap(); EList<Property> newProperties = file.getProperties(); if(hasPermission(getModelObject(), getSession(), CommonPermissions.ACTION_EDIT)) { boolean hadChanges = false; for (Property newProp : newProperties) { Property origProp = originalProperties.get(newProp.getKey()); if(origProp==null) continue; if(newProp.getValue()!=null && !newProp.getValue().isEmpty() && !newProp.getValue().equals(origProp.getValue())) { hadChanges = true; origProp.setValue(newProp.getValue()); origProp.setComment(newProp.getComment()); } } if(hadChanges) persistenceService.saveProperties(getModelObject(), original); } else if(hasPermission(getModelObject(), getSession(), CommonPermissions.ACTION_SUGGEST)) { final List<Review> reviews = new ArrayList<Review>(); for (Property newProp : newProperties) { Property origProp = originalProperties.get(newProp.getKey()); if(origProp==null) continue; if(newProp.getValue()!=null && !newProp.getValue().isEmpty() && !newProp.getValue().equals(origProp.getValue())) { final Review review = PropertiesFactory.eINSTANCE.createReview(); review.setCreated(System.currentTimeMillis()); review.setKey(newProp.getKey()); review.setMessage(newProp.getValue()); review.setSeverity(Severity.INFO); review.setReviewType(Review.KIND_SUGGESTION); String username = CommonPermissions.USER_ANONYMOUS; Session session = getSession(); if (session instanceof CDOAuthenticatedSession) { CDOAuthenticatedSession authSession = (CDOAuthenticatedSession) session; User user = authSession.getUser(); if (user != null) username = user.getName(); } review.setUser(username); if (newProp.getComment() != null && !newProp.getComment().isEmpty()) { Comment comment = PropertiesFactory.eINSTANCE.createComment(); comment.setUser(username); comment.setCreated(review.getCreated()); comment.setMessage(newProp.getComment()); review.getComments().add(comment); } reviews.add(review); } } if (!reviews.isEmpty()) { TransactionUtil.commit(getModelObject(), new Modification<PropertyFileDescriptor, PropertyFileDescriptor>() { @Override public PropertyFileDescriptor apply(PropertyFileDescriptor object) { object.getReviews().addAll(reviews); return object; } }); } } try { Thread.sleep(500); } catch (InterruptedException e) { } getSession().success(new StringResourceModel("message.upload.successfull", this, null).getString()); setResponsePage(ResourcePage.class, getPage().getPageParameters()); } catch (ExecutionException e) { LOG.error("Failed to merge uploaded properties with "+getModelObject().getLocation(),e); getSession().success(new StringResourceModel("message.upload.failed", this, null,e.getMessage()).getString()); } catch (CommitException e) { logger.error("Commit of suggestion failed", e); getSession().success(new StringResourceModel("message.upload.failed", this, null,e.getMessage()).getString()); } } } class PropertyPairListDataProvider extends SortableDataProvider<PropertyPair, EClassSortState> implements IFilterStateLocator<String> { private static final long serialVersionUID = 1L; private CompoundPropertyModel<PropertyFileDescriptor> model; private transient List<PropertyPair> contents; private String filterState; private PropertyListMode mode; private IModel<Multimap<String, Review>> reviewModel; public PropertyPairListDataProvider(PropertyFileDescriptor descriptor, PropertyListMode mode, IModel<Multimap<String, Review>> reviewModel) { super(); model = new CompoundPropertyModel<PropertyFileDescriptor>(new EObjectModel<PropertyFileDescriptor>(descriptor)); this.mode = mode; this.reviewModel = reviewModel; } @Override public Iterator< ? extends PropertyPair> iterator(long first, long count) { List<PropertyPair> contents = getList(); return contents.subList((int)first, (int)first + (int)count).iterator(); } private List<PropertyPair> getList() { if (contents == null) { contents = createContents(); } return contents; } protected List<PropertyPair> createContents() { PropertyFileDescriptor descriptor = model.getObject(); Multimap<String, Review> reviews = reviewModel.getObject(); PropertyFileDescriptor master = descriptor.isMaster() ? descriptor : descriptor.getMaster() ; Map<String, Property> translated = new HashMap<String, Property>(loadProperties(descriptor).asMap()); PropertyFile templateFile = loadProperties(master); List<PropertyPair> contents = new ArrayList<PropertyPair>(); for (Property property : templateFile.getProperties()) { // IModel<String> bind = model.bind(property.getKey()); // bind.set PropertyPair pair = new PropertyPair(property, translated.remove(property.getKey()), descriptor.getVariant(), descriptor.cdoID()); String key = pair.getKey(); if (mode.apply(pair, reviews.get(key))) contents.add(pair); } for (Property property : translated.values()) { PropertyPair pair = new PropertyPair(null, property, descriptor.getVariant(), descriptor.cdoID()); if (mode.apply(pair, reviews.get(pair.getKey()))) contents.add(pair); } return contents; } private PropertyFile loadProperties(PropertyFileDescriptor descriptor) { try { return propertyPersistence.loadProperties(descriptor); } catch (ExecutionException e) { logger.error("Failed to load properties for "+descriptor); throw new AbortWithHttpErrorCodeException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,"Failed to load properties for "+descriptor); } } @Override public long size() { return getList().size(); } @Override public IModel<PropertyPair> model(PropertyPair object) { return Model.of(object); } @Override public void setFilterState(String state) { this.filterState = state; } @Override public String getFilterState() { return filterState; } } } class DeleteLink extends StatelessLink<Void>{ private static final long serialVersionUID = 8205155656605708520L; @Inject private URIResolver resolver; private static final Logger LOG = LoggerFactory.getLogger(DeleteLink.class); public DeleteLink(String id) { super(id); } @Override public void onClick() { PropertyFileDescriptor descriptor = getModel(getPage().getPageParameters()); if(descriptor!=null){ try { ProjectLocale locale = TransactionUtil.commit(descriptor, new Modification<PropertyFileDescriptor, ProjectLocale>() { @Override public ProjectLocale apply(PropertyFileDescriptor object) { ProjectLocale locale = object.getProjectLocale(); if(!object.isMaster()) object = object.getMaster(); if(object!=null) PropertyResourceUtil.removeDescriptor(object); return locale; }; }); URI uri = resolver.getURI(locale); PageParameters params = new PageParameters(); for(int i=0;i<uri.segmentCount();i++) { params.set(i, uri.segment(i)); } //go to the parent locale setResponsePage(ResourcePage.class, params); } catch (CommitException e) { error(e.getMessage()); LOG.error("Failed to delete descriptor",e); } } } private PropertyFileDescriptor getModel(PageParameters pageParameters) { URI uri = URI.createURI("/"); for(int i=0;i<pageParameters.getIndexedCount();i++){ uri = uri.appendSegment(pageParameters.get(i).toString()); } return (PropertyFileDescriptor) resolver.resolve(uri); } }